/*******************************************************************************
 * Copyright (c) 2010 European Software Institute - Tecnalia.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 * Contributors:
 *     Author - Adri�n Noguero (adrian.noguero@esi.es)
 *     
 *******************************************************************************/

package es.esi.gemde.modelvalidator.service.impl;

import java.io.File;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.util.HashMap;
import java.util.List;
import java.util.Vector;

import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.core.runtime.IConfigurationElement;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Platform;
import org.eclipse.emf.ecore.EObject;
import org.eclipse.jface.dialogs.MessageDialog;
import org.eclipse.swt.widgets.Shell;
import org.eclipse.ui.IMemento;
import org.eclipse.ui.XMLMemento;

import es.esi.gemde.core.resources.IGemdeResource.ResourceType;
import es.esi.gemde.modelvalidator.ModelValidatorPlugin;
import es.esi.gemde.modelvalidator.exceptions.IllegalTargetException;
import es.esi.gemde.modelvalidator.exceptions.NoConstraintsValidatedException;
import es.esi.gemde.modelvalidator.exceptions.ValidationEngineException;
import es.esi.gemde.modelvalidator.service.BatchValidationFactory;
import es.esi.gemde.modelvalidator.service.IBatchValidation;
import es.esi.gemde.modelvalidator.service.IConstraintFactory;
import es.esi.gemde.modelvalidator.service.IModelValidatorConstraint;
import es.esi.gemde.modelvalidator.service.IModelValidatorService;
import es.esi.gemde.modelvalidator.service.IValidationResult;
import es.esi.gemde.modelvalidator.service.OCLConstraintFactory;


/**
 * Implementation of the {@link IModelValidatorService} interface.
 * This class holds all the logic of the GEMDE Model Validation Service
 *
 * @author Adri�n Noguero (adrian.noguero@esi.es)
 * @version 1.0
 * @since 1.0
 *
 */
public class ModelValidatorService implements IModelValidatorService {

	private List<IModelValidatorConstraint> constraintRepository;
	private List<IBatchValidation> validationRepository;
	private IValidationResult lastValidationResult;
	private File repositoryFile;
	
	/**
	 * Singleton instance of the service
	 */
	private static final ModelValidatorService myself = new ModelValidatorService();
	
	
	/**
	 * Constants for repository management
	 */
	private final String ROOT = "ModelValidatorRepository";
	private final String CONSTRAINT = "Constraint";
	private final String VALIDATION = "Validation";
	private final String NAME = "name";
	private final String CATEGORY = "category";
	private final String SEVERITY = "severity";
	private final String BODY = "body";
	private final String REFERENCE = "ReferencedConstraint";
	public static final String SESSION_DATA_EXTENSION_ID = "es.esi.gemde.modelvalidator.sessiondata";
	
	/**
	 * Constructor intended to be used internally only.
	 *
	 */
	private ModelValidatorService () {
		constraintRepository = new Vector<IModelValidatorConstraint>();
		validationRepository = new Vector<IBatchValidation>();
		lastValidationResult = null;
		repositoryFile = null;
	}
	
	/**
	 * Method that initializes the repositories with information
	 * from the repository directory
	 */
	public void initializeRepositories() {
		
		IPath wsPath = ResourcesPlugin.getWorkspace().getRoot().getLocation();
		
		// Create the directories if needed
		File dir = wsPath.append(".metadata/.plugins").toFile();
		if (!dir.exists() || !dir.isDirectory()) {
			dir.mkdir();
		}
		
		dir = wsPath.append(".metadata/.plugins/" + ModelValidatorPlugin.PLUGIN_ID).toFile();
		if (!dir.exists() || !dir.isDirectory()) {
			dir.mkdir();
		}
		
		repositoryFile = ResourcesPlugin.getWorkspace().getRoot().getLocation().append(".metadata/.plugins/" + ModelValidatorPlugin.PLUGIN_ID + "/validation_repository.xml").toFile();
		if (repositoryFile != null && !repositoryFile.exists()) {
			try {
				repositoryFile.createNewFile();
			} catch (IOException e) {
				MessageDialog.openError(new Shell(), "IOExceptionOccurred", e.getMessage());
				e.printStackTrace();
			}
		}
		else {
			FileReader reader;
			XMLMemento memento;
			IConstraintFactory factory = new OCLConstraintFactory();
			try {
				reader = new FileReader(repositoryFile);
				memento = XMLMemento.createReadRoot(reader);
				
				// Initialize Constraints
				IMemento[] constraints = memento.getChildren(CONSTRAINT);
				for (int i = 0; i < constraints.length; i++) {
					String name = constraints[i].getString(NAME);
					String category = constraints[i].getString(CATEGORY);
					int severity = constraints[i].getInteger(SEVERITY);
					String body = constraints[i].getString(BODY);
					
					// Currently only OCL constraints are supported!!
					addConstraint(factory.createConstraint(name, category, severity, body));
					
				}
				
				// Initialize Validations
				IMemento[] validations = memento.getChildren(VALIDATION);
				for (int i = 0; i < validations.length; i++) {
					String validationName = validations[i].getString(NAME);
					List<IModelValidatorConstraint> selectedConstraints = new Vector<IModelValidatorConstraint>();
					IMemento[] referencedConstraints = validations[i].getChildren(REFERENCE);
					for (int j = 0; j < referencedConstraints.length; j++) {
						selectedConstraints.add(getConstraintByName(referencedConstraints[j].getString(NAME)));
					}
					
					// If a validation has already been contributed rename to avoid losing data!
					try {
						addBatchValidation(BatchValidationFactory.createBatchValidation(validationName, selectedConstraints, selectedConstraints.get(0).getApplicationMetamodel()));
					}
					catch (IllegalArgumentException e) {
						addBatchValidation(BatchValidationFactory.createBatchValidation(validationName + "*", selectedConstraints, selectedConstraints.get(0).getApplicationMetamodel()));
					}
				}
			}
			catch (Exception e) {
				e.printStackTrace();
			}
		}
		
	}


	/**
	 * A internal method used to locate a constraint in the repository by its name
	 * 
	 * @param name the name of the constraint we want to locate
	 * @return the located constraint or null if the constraint couldn't be located
	 */
	private IModelValidatorConstraint getConstraintByName(String name) {
		for (IModelValidatorConstraint constraint : constraintRepository) {
			if (constraint.getName().equals(name)) {
				return constraint;
			}
		}
		return null;
	}

	/* (non-Javadoc)
	 * @see es.esi.gemde.modelvalidator.service.IModelValidatorService#saveRepositories()
	 */
	@Override
	public synchronized void saveRepositories() {
		XMLMemento memento = XMLMemento.createWriteRoot(ROOT);
		for (IModelValidatorConstraint constraint : constraintRepository) {
			if (constraint.getResourceType().equals(ResourceType.NORMAL)) {
				IMemento child = memento.createChild(CONSTRAINT);
				child.putString(NAME, constraint.getName());
				child.putString(CATEGORY, constraint.getCategory());
				child.putInteger(SEVERITY, constraint.getSeverity());
				child.putString(BODY, constraint.getBody());
			}
		}
		
		for (IBatchValidation validation : validationRepository) {
			if (validation.getResourceType().equals(ResourceType.NORMAL)) {
				IMemento child = memento.createChild(VALIDATION);
				child.putString(NAME, validation.getName());
				for (IModelValidatorConstraint reference : validation.getSelectedConstraints()) {
					IMemento subChild = child.createChild(REFERENCE);
					subChild.putString(NAME, reference.getName());
				}
			}
		}
		try {
			memento.save(new FileWriter(repositoryFile));
		} catch (IOException e) {
			e.printStackTrace();
		}
	}
	
	/**
	 * A static method to access the singleton instance of the service
	 * @return the instance of the service
	 */
	public static IModelValidatorService getInstance() {
		return myself;
	}

	/* (non-Javadoc)
	 * @see es.esi.gemde.modelvalidator.service.IModelValidatorService#addBatchValidation(es.esi.gemde.modelvalidator.service.BatchValidation)
	 */
	@Override
	public void addBatchValidation(IBatchValidation validation) throws IllegalArgumentException {
		if (validation == null) {
			throw new IllegalArgumentException("A Null BatchValidation was provided");
		}
		for (IBatchValidation curr : validationRepository) {
			if (curr.getName() == validation.getName()) {
				throw new IllegalArgumentException("Provided BatchValidation already exists");
			}
		}
		validationRepository.add(validation);
	}

	/* (non-Javadoc)
	 * @see es.esi.gemde.modelvalidator.service.IModelValidatorService#addSessionBatchValidation(es.esi.gemde.modelvalidator.service.IBatchValidation)
	 */
	@Override
	public void addSessionBatchValidation(IBatchValidation validation)
			throws IllegalArgumentException {
		if (!validation.getResourceType().equals(ResourceType.PROTECTED)) {
			throw new IllegalArgumentException("The provided session validation is not protected");
		}
		addBatchValidation(validation);
	}

	/* (non-Javadoc)
	 * @see es.esi.gemde.modelvalidator.service.IModelValidatorService#addConstraint(es.esi.gemde.modelvalidator.service.ModelValidatorConstraint)
	 */
	@Override
	public void addConstraint(IModelValidatorConstraint constraint) throws IllegalArgumentException {
		if (constraint == null) {
			throw new IllegalArgumentException("A Null constraint was provided");
		}
		for (IModelValidatorConstraint curr : constraintRepository) {
			if (curr.getName() == constraint.getName() && curr.getCategory() == constraint.getCategory()) {
				throw new IllegalArgumentException("Provided constraint already exists");
			}
		}
		constraintRepository.add(constraint);
	}

	@Override
	public void addSessionConstraint(IModelValidatorConstraint constraint)
			throws IllegalArgumentException {
		if (!constraint.getResourceType().equals(ResourceType.PROTECTED)) {
			throw new IllegalArgumentException("The provided session constraint is not protected");
		}
		addConstraint(constraint);
	}

	/* (non-Javadoc)
	 * @see es.esi.gemde.modelvalidator.service.IModelValidatorService#changeBatchValidation(es.esi.gemde.modelvalidator.service.BatchValidation)
	 */
	@Override
	public void changeBatchValidation(IBatchValidation validation) throws IllegalArgumentException {
		if (validation == null) {
			throw new IllegalArgumentException("A null validation was provided");
		}
		int i = 0;
		boolean found = false;
		while (i < validationRepository.size()&& !found) {
			if (validationRepository.get(i).getName() == validation.getName()) {
				found = true;
			}
			else {
				i++;
			}
		}
		if (found) {
			validationRepository.remove(i);
			validationRepository.add(validation);
		}
		else {
			throw new IllegalArgumentException("No validation exists with the same name as the provided validation");
		}

	}

	/* (non-Javadoc)
	 * @see es.esi.gemde.modelvalidator.service.IModelValidatorService#changeConstraint(es.esi.gemde.modelvalidator.service.ModelValidatorConstraint)
	 */
	@Override
	public void changeConstraint(IModelValidatorConstraint constraint) throws IllegalArgumentException {
		if (constraint == null) {
			throw new IllegalArgumentException("A null constraint was provided");
		}
		int i = 0;
		boolean found = false;
		while (i < constraintRepository.size()&& !found) {
			if (constraintRepository.get(i).getName() == constraint.getName() && constraintRepository.get(i).getCategory() == constraint.getCategory()) {
				found = true;
			}
			else {
				i++;
			}
		}
		if (found) {
			constraintRepository.remove(i);
			constraintRepository.add(constraint);
		}
		else {
			throw new IllegalArgumentException("No constraint exists with the same name and category as the provided constraint");
		}

	}

	@Override
	public IBatchValidation getBatchValidation(String name) {
		for (IBatchValidation val : validationRepository) {
			if (val.getName().equals(name)) {
				return val;
			}
		}
		return null;
	}

	/* (non-Javadoc)
	 * @see es.esi.gemde.modelvalidator.service.IModelValidatorService#getBatchValidationRepository()
	 */
	@Override
	public List<IBatchValidation> getBatchValidationRepository() {
		List<IBatchValidation> result = new Vector<IBatchValidation>();
		for (IBatchValidation val : validationRepository) {
			result.add(val);
		}
		return result;
	}

	/* (non-Javadoc)
	 * @see es.esi.gemde.modelvalidator.service.IModelValidatorService#getConstraintRepository()
	 */
	@Override
	public List<IModelValidatorConstraint> getConstraintRepository() {
		List<IModelValidatorConstraint> result = new Vector<IModelValidatorConstraint>();
		for (IModelValidatorConstraint con : constraintRepository) {
			result.add(con);
		}
		return result;
	}

	/* (non-Javadoc)
	 * @see es.esi.gemde.modelvalidator.service.IModelValidatorService#getLastValidationResult()
	 */
	@Override
	public IValidationResult getLastValidationResult() {
		return lastValidationResult;
	}

	/* (non-Javadoc)
	 * @see es.esi.gemde.modelvalidator.service.IModelValidatorService#removeBatchValidation(java.lang.String)
	 */
	@Override
	public void removeBatchValidation(String validationName) throws IllegalArgumentException {
		if (validationName == null) {
			throw new IllegalArgumentException("A null name was provided");
		}
		int i = 0;
		boolean found = false;
		while (i < validationRepository.size()&& !found) {
			if (validationRepository.get(i).getName().equals(validationName)) {
				found = true;
			}
			else {
				i++;
			}
		}
		if (found) {
			validationRepository.remove(i);
		}

	}

	/* (non-Javadoc)
	 * @see es.esi.gemde.modelvalidator.service.IModelValidatorService#removeConstraint(java.lang.String, java.lang.String)
	 */
	@Override
	public void removeConstraint(String name, String category) throws IllegalArgumentException {
		if (name == null || category == null) {
			throw new IllegalArgumentException("A null parameter was provided");
		}
		
		
		int i = 0;
		boolean found = false;
		while (i < constraintRepository.size()&& !found) {
			if (constraintRepository.get(i).getName() == name && constraintRepository.get(i).getCategory() == category) {
				found = true;
			}
			else {
				i++;
			}
		}
		if (found) {
			// Check if the removed constraint is part of a registered validation!
			boolean used = false;
			for (IBatchValidation valid : validationRepository) {
				if (valid.getSelectedConstraints().contains(constraintRepository.get(i))) {
					used = true;
				}
			}
			if (used) {
				throw new IllegalArgumentException("Removed constraint is in use");
			}
			else {
				constraintRepository.remove(i);
			}
		}
		else {
			throw new IllegalArgumentException("Selected constraint doesn't exist");
		}

	}

	/* (non-Javadoc)
	 * @see es.esi.gemde.modelvalidator.service.IModelValidatorService#runValidation(java.lang.String)
	 */
	@Override
	public IValidationResult runValidation(String validationName, List<EObject> targets) throws ValidationEngineException, IllegalTargetException, IllegalArgumentException, NoConstraintsValidatedException {
		if (validationName == null) {
			throw new IllegalArgumentException("A null name was provided");
		}
		if (targets == null || targets.size() == 0) {
			throw new IllegalArgumentException("No targets were provided");
		}
		int i = 0;
		boolean found = false;
		while (i < validationRepository.size()&& !found) {
			if (validationRepository.get(i).getName().equals(validationName)) {
				found = true;
			}
			else {
				i++;
			}
		}
		if (found) {
			lastValidationResult = validationRepository.get(i).execute(targets);
			return lastValidationResult;
		}
		else {
			throw new IllegalArgumentException("Provided validation name does not exist in the repository");
		}
	}

	/* (non-Javadoc)
	 * @see es.esi.gemde.modelvalidator.service.IModelValidatorService#getConstraintCategories()
	 */
	@Override
	public List<String> getConstraintCategories() {
		Vector<String> result = new Vector<String>();
		
		for (IModelValidatorConstraint constraint : constraintRepository) {
			if (!result.contains(constraint.getCategory())) {
				result.add(constraint.getCategory());
			}
		}
		
		return result;
	}
	
	/**
	 * Method called by the plug-in during start up to load all the
	 * session constraints and validations contributed by other plug-ins.
	 */
	public void loadExtensions() {
		// Get the elements contributing in the extension point
		IConfigurationElement[] config = Platform.getExtensionRegistry().getConfigurationElementsFor(SESSION_DATA_EXTENSION_ID);
		// Session Data elements
		for (IConfigurationElement sessionData : config) {
			try {
				HashMap<String,IModelValidatorConstraint> createdConstraints = new HashMap<String,IModelValidatorConstraint>();
				Vector<IBatchValidation> createdValidations = new Vector<IBatchValidation>();
				OCLConstraintFactory factory = new OCLConstraintFactory();
				
				// Contributed Categories
				IConfigurationElement[] categories = sessionData.getChildren("constraint_category");
				for (IConfigurationElement cat : categories) {
					IConfigurationElement[] constraints = cat.getChildren("text_constraint");
					for (IConfigurationElement constraint : constraints) {
						final String id = constraint.getAttribute("id");
						String sev = constraint.getAttribute("severity");
						int severity;
						if (sev.equals("INFO")) {
							severity = IStatus.INFO;
						}
						else if (sev.equals("WARNING")) {
							severity = IStatus.WARNING;
						}
						else {
							severity = IStatus.ERROR;
						}
						
						createdConstraints.put(id, factory.createConstraint(constraint.getAttribute("name"), cat.getAttribute("name"), severity, constraint.getAttribute("body"), true));
					}
					IConfigurationElement [] classConstraints = cat.getChildren("java_constraint");
					for (IConfigurationElement constraint : classConstraints) {
						final Object impl = constraint.createExecutableExtension("implementation");
						final String id = constraint.getAttribute("id");
						if (checkImpl(impl)) {
							createdConstraints.put(id, (IModelValidatorConstraint)impl);
						}
					}
				}
				
				for (IModelValidatorConstraint constraint : createdConstraints.values()) {
					addSessionConstraint(constraint);
				}
				
				// Contributed Validations
				IConfigurationElement[] validations = sessionData.getChildren("validation");
				for (IConfigurationElement val : validations) {
					Vector<IModelValidatorConstraint> selectedConstraints = new Vector<IModelValidatorConstraint>();
					IConfigurationElement[] selected = val.getChildren("constraint_reference");
					for (IConfigurationElement constraint : selected) {
						selectedConstraints.add(createdConstraints.get(constraint.getAttribute("id")));
					}
					if (selectedConstraints.size() > 0) {
						createdValidations.add(BatchValidationFactory.createProtectedBatchValidation(val.getAttribute("name"), selectedConstraints, selectedConstraints.get(0).getApplicationMetamodel()));
					}
				}
				
				for (IBatchValidation validation : createdValidations) {
					addSessionBatchValidation(validation);
				}
				
				
			}
			catch(Exception e) {
				e.printStackTrace();
			}
			
		}
	}

	// Convenience method to check the validity of the contributed constraint
	private boolean checkImpl(Object impl) {
		if (!(impl instanceof IModelValidatorConstraint)) {
			return false;
		}
		
		IModelValidatorConstraint constraint = (IModelValidatorConstraint)impl;
		
		if (constraint.getApplicationContext() == null) {
			return false;
		}
		
		if (constraint.getApplicationMetamodel() == null) {
			return false;
		}
		
		if (constraint.getSeverity() != IStatus.INFO && constraint.getSeverity() != IStatus.ERROR && constraint.getSeverity() != IStatus.WARNING) {
			return false;
		}
		
		if (constraint.getName() == null) {
			return false;
		}
		
		if (constraint.getCategory() == null) {
			return false;
		}
		
		return constraint.getResourceType().equals(ResourceType.PROTECTED);
	}

}
